package websocket;

import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.util.logging.Level;
import java.util.logging.Logger;

public class Framedata
{
    private final static Logger LOGGER = Logger.getLogger(Framedata.class
        .getName());
    
    private boolean fin;
    private boolean rsv1;
    private boolean rsv2;
    private boolean rsv3;
    private Opcode opcode;
    private boolean mask;
    private long payloadLength;
    private byte[] maskingKey;
    private byte[] payloadData;
    
    public static final int FRAME_HEADER_MAX_LENGTH = 14; // Specifies how long may be the header of the frame
    public static final int FRAME_CONTENT_MAX_LENGTH = 524288 - FRAME_HEADER_MAX_LENGTH; // 512KB at most
    
    private Framedata() {} 
    
    public Framedata(boolean fin, Opcode opcode, byte[] data)
    {
        this.fin = fin;
        // No extensions supported - RSV bits should be set to 0
        this.rsv1 = false;
        this.rsv2 = false;
        this.rsv3 = false;
        this.opcode = opcode;
        // Masking response by server is forbidden
        this.mask = false;
        
        this.payloadData = new byte[data.length];
        System.arraycopy(data, 0, this.payloadData, 0, data.length);
        this.payloadLength = data.length;
    }
    
    public static Framedata[] convertMessageToFrames(String message)
    {
        // Method generates a set of frames to send the passed message
        LOGGER.log(Level.FINER, "Converting bellow message to frames:\n{0}", message);
        
        byte[] data = message.getBytes();     
        int framesAmount = (int) Math.ceil((double) data.length / FRAME_CONTENT_MAX_LENGTH);
        Framedata[] result = new Framedata[framesAmount];
        Framedata frame;
        
        LOGGER.log(Level.FINEST, "{0} frames will be created", framesAmount);
        
        if (framesAmount == 1)
        {
            result[0] = new Framedata(true, Opcode.TEXT_FRAME, data);
        }
        else
        {
            byte[] buffer = new byte[FRAME_CONTENT_MAX_LENGTH];
        
            System.arraycopy(data, 0, buffer, 0, FRAME_CONTENT_MAX_LENGTH);
            result[0] = new Framedata(false, Opcode.TEXT_FRAME, buffer);
            
            for (int i = 1; i < framesAmount - 1; i++)
            {
                System.arraycopy(data, i * FRAME_CONTENT_MAX_LENGTH, buffer, 0, FRAME_CONTENT_MAX_LENGTH);
                result[i] = new Framedata(false, Opcode.CONTINUATION_FRAME, buffer);
            }
            
            buffer = new byte[data.length - ((framesAmount - 1) * FRAME_CONTENT_MAX_LENGTH)];
            System.arraycopy(data, (framesAmount - 1) * FRAME_CONTENT_MAX_LENGTH,
                                   buffer, 0, buffer.length);
            result[framesAmount - 1] = new Framedata(true, Opcode.CONTINUATION_FRAME, buffer);
        }
        
        return result;
    }
    
    public static Framedata getFrame(InputStream in) throws IOException, WebSocketProtocolException
    {
        if (in == null)
        {
            throw new IllegalArgumentException("Input stream cannot be null");
        }
        
        LOGGER.fine("Reading frame");
        
        Framedata frame = new Framedata();
        
        byte[] buffer = new byte[1024];
        Framedata.readBytes(in, buffer, 0, 2);
        
        frame.fin = (buffer[0] & 0x80) > 0 ? true : false;
        frame.rsv1 = (buffer[0] & 0x40) > 0 ? true : false;
        frame.rsv2 = (buffer[0] & 0x20) > 0 ? true : false;
        frame.rsv3 = (buffer[0] & 0x10) > 0 ? true : false;
        frame.mask = (buffer[1] & 0x80) > 0 ? true : false;
        
        frame.opcode = Opcode.toOpcode((byte) (buffer[0] & 0x0F));
        if (frame.opcode == Opcode.NOT_SUPPORTED)
        {
            throw new WebSocketProtocolException("The opcode " + frame.opcode.code + " is not a valid websocket opcode");
            //System.out.println("The opcode " + frame.opcode.code + " is not a valid websocket opcode");
        }
        
        frame.payloadLength = buffer[1] & 0x7F;
        // All control frames MUST have a payload length of 125 bytes or less and MUST NOT be fragmented.
        if (frame.payloadLength > 126 && ! frame.isDataFrame())
        {
            throw new WebSocketProtocolException(
                    "All control frames MUST have a payload length of 125 bytes or less and MUST NOT be fragmented.");
        }
        
        if (frame.payloadLength == 126)
        {
            // Extended payload length to 16 bits
            Framedata.readBytes(in, buffer, 0, 2);
            
            frame.payloadLength = buffer[0] << 8 | (buffer[1] & 0xFF);
        }
        else if (frame.payloadLength == 127)
        {
            // Extended payload length to 64 bits
            Framedata.readBytes(in, buffer, 0, 8);
            
            frame.payloadLength = ByteBuffer.wrap(buffer, 0, 8).getLong();
            
            if (frame.payloadLength > FRAME_CONTENT_MAX_LENGTH)
            {
                // Data of this length is not supported by our server
                throw new UnsupportedOperationException("Message of length " + frame.payloadLength
                    + " is not supported. Maximum length supported: "
                    + (FRAME_CONTENT_MAX_LENGTH));
            }
        }
        
        if (frame.mask)
        {
            // Read the mask
            Framedata.readBytes(in, buffer, 0, 4);
            
            frame.maskingKey = new byte[4];
            frame.maskingKey[0] = buffer[0];
            frame.maskingKey[1] = buffer[1];
            frame.maskingKey[2] = buffer[2];
            frame.maskingKey[3] = buffer[3];
        }
        
        frame.payloadData = new byte[(int) frame.payloadLength];
        Framedata.readBytes(in, frame.payloadData, 0, (int) frame.payloadLength);
        if (frame.mask)
        {
            for (int i=0; i < (int) frame.payloadLength; i++)
            {
                frame.payloadData[i] ^= frame.maskingKey[i % 4];
            }
        }
        
        
        LOGGER.log(Level.FINE, "Frame of type {0} read. Data length is {1} bytes",
                               new Object[]{frame.opcode, frame.payloadData.length});
        LOGGER.log(Level.FINEST, "Read data:\n{0}", frame.payloadData);
        
        return frame;
    }
    
    private static void readBytes(InputStream in, byte[] buffer, int off, int len) throws IOException
    {
        int bytesLeft = len;
        int bytesRead = 0;
        while (bytesLeft > 0)
        {
            bytesRead = in.read(buffer, off + (len - bytesLeft), bytesLeft);
            if (bytesRead == -1)
            {
                throw new IOException("Unexpected end of stream");
            }
            
            bytesLeft -= bytesRead;
        }
    }
    
    public byte[] getBytes()
    {
        int length = 2; // Length of frame in bytes
        if (payloadData.length > 125)
        {
            length += 2;
        }
        if (payloadData.length > FRAME_CONTENT_MAX_LENGTH)
        {
            // Actually it is not supported and this condition should never be true
            throw new UnsupportedOperationException("Message of length " + payloadData.length
                    + " is not supported. Maximum length supported: "
                    + (FRAME_CONTENT_MAX_LENGTH));
        }
        
        if (mask)
        {
            length += 4;
        }
        length += payloadData.length;
        
        byte[] frame = new byte[length];
        
        if (fin)
        {
            frame[0] |= 0x80;
        }
        if (rsv1)
        {
            frame[0] |= 0x40;
        }
        if (rsv2)
        {
            frame[0] |= 0x20;
        }
        if (rsv3)
        {
            frame[0] |= 0x10;
        }
        
        frame[0] |= opcode.code;
        
        if (mask)
        {
            frame[1] |= 0x80;
        }
        
        int byteIterator = 2;
        if (payloadData.length < 126)
        {
            frame[1] = (byte) payloadData.length;
        }
        else if (payloadData.length < 65536) // 2^16, value, which exceeds 16 bit register
        {
            frame[1] = (byte) 126;
            frame[2] = (byte) (payloadData.length >> 8);
            frame[3] = (byte) (payloadData.length);
            byteIterator = 4;
        }
        else
        {
            frame[1] = 127;
            frame[2] = 0;
            frame[3] = 0;
            frame[4] = 0;
            frame[5] = 0;
            frame[6] = (byte) (payloadData.length >> 24);
            frame[7] = (byte) (payloadData.length >> 16);
            frame[8] = (byte) (payloadData.length >> 8);
            frame[9] = (byte) (payloadData.length);
            byteIterator = 10;
        }
        
        if (mask)
        {
            frame[1] |= 0x80;
            frame[byteIterator] = maskingKey[0];
            frame[byteIterator + 1] = maskingKey[1];
            frame[byteIterator + 2] = maskingKey[2];
            frame[byteIterator + 3] = maskingKey[3];
            byteIterator += 4;
        }
        
        if (mask)
        {
            for (int i = 0; i < payloadData.length; i++)
            {
                // Masking
                frame[byteIterator + i] = (byte) (payloadData[i] ^ maskingKey[i % 4]);
            }
        }
        else
        {
            System.arraycopy(payloadData, 0, frame, byteIterator, payloadData.length);
        }

        return frame;
    }
    
    
    
    
    
    public static Framedata generatePingFrame()
    {
        return new Framedata(true, Opcode.PING_FRAME, new byte[0]);
    }
    
    public static Framedata generatePongFrame()
    {
        return new Framedata(true, Opcode.PONG_FRAME, new byte[0]);
    }
    
    public static Framedata generateCloseFrame(ClosingReason reason)
    {
        if (reason == ClosingReason.NO_STATUS_RECEIVED
                || reason == ClosingReason.ABNORMAL_CLOSURE
                || reason == ClosingReason.TLS_HANDSHAKE_FAILURE)
        {
            throw new IllegalArgumentException("The reason " + reason + " MUST NOT be used according to RFC 6455");
        }
        
        byte[] code = new byte[2];
        code[0] = (byte) (reason.code >> 8);
        code[1] = (byte) reason.code;
        return new Framedata(true, Opcode.CONNECTION_CLOSE_FRAME, code);
    }
    
    public static Framedata generateCloseFrame()
    {
        return new Framedata(true, Opcode.CONNECTION_CLOSE_FRAME, new byte[0]);
    }
    
    public enum Opcode
    {
        CONTINUATION_FRAME(0),
        TEXT_FRAME(1),
        BINARY_FRAME(2),
        CONNECTION_CLOSE_FRAME(8),
        PING_FRAME(9),
        PONG_FRAME(10),
        NOT_SUPPORTED(15);
        
        private byte code;
        
        private Opcode(int code)
        {
            this.code = (byte) code;
        }
        
        public static Opcode toOpcode(byte opcode)
	    {
            Opcode val = NOT_SUPPORTED;
	        switch(opcode)
            {
                case 0:
                    val = CONTINUATION_FRAME;
                    break;
                case 1:
                    val = TEXT_FRAME;
                    break;
                case 2:
                    val = BINARY_FRAME;
                    break;
                case 8:
                    val = CONNECTION_CLOSE_FRAME;
                    break;
                case 9:
                    val = PING_FRAME;
                    break;
                case 10:
                    val = PONG_FRAME;
                    break; 
            }
            return val;
	    }
        
    }
    
    public Opcode getOpcode()
    {
        return opcode;
    }
    
    public boolean isDataFrame()
    {
        return opcode == Opcode.BINARY_FRAME || opcode == Opcode.TEXT_FRAME;
    }
    
    public boolean isBinaryDataFrame()
    {
        return opcode == Opcode.BINARY_FRAME;
    }
    
    public boolean isTextDataFrame()
    {
        return opcode == Opcode.TEXT_FRAME;
    }
    
    public boolean isPingFrame()
    {
        return opcode == Opcode.PING_FRAME;
    }
    
    public boolean isPongFrame()
    {
        return opcode == Opcode.PONG_FRAME;
    }
    
    public boolean isCloseFrame()
    {
        return opcode == Opcode.CONNECTION_CLOSE_FRAME;
    }
    
    public boolean isContinuationFrame()
    {
        return opcode == Opcode.CONTINUATION_FRAME;
    }
    
    public boolean isFinFrame()
    {
        return fin;
    }

    public byte[] getData()
    {
        return payloadData;
    }
    
    public String getTextData()
    {
        String textData = "";
        try
        {
            textData = new String(payloadData, "UTF-8");
        }
        catch (UnsupportedEncodingException ex)
        {
            LOGGER.severe(ex.toString());
        }
        return textData;
    }
    
    public enum ClosingReason
    {
        NORMAL_CLOSURE(1000),
        GOING_AWAY(1001),
        PROTOCOL_ERROR(1002),
        UNSUPPORTED_DATA(1003),
        NO_STATUS_RECEIVED(1005),
        ABNORMAL_CLOSURE(1006),
        INVALID_FRAME_PAYLOAD_DATA(1007),
        POLICY_VIOLATION(1008),
        MESSAGE_TOO_BIG(1009),
        MANDATORY_EXTENSION(1010),
        INTERNAL_SERVER_ERROR(1011),
        TLS_HANDSHAKE_FAILURE(1015);
        
        private int code;
        
        private ClosingReason(int code)
        {
            this.code = code;
        }
        
        public static ClosingReason toClosingReason(int code)
	    {
            ClosingReason val = NO_STATUS_RECEIVED;
	        switch(code)
            {
                case 1000:
                    val = NORMAL_CLOSURE;
                    break;
                case 1001:
                    val = GOING_AWAY;
                    break;
                case 1002:
                    val = PROTOCOL_ERROR;
                    break;
                case 1003:
                    val = UNSUPPORTED_DATA;
                    break;
                case 1005:
                    val = NO_STATUS_RECEIVED;
                    break;
                case 1006:
                    val = ABNORMAL_CLOSURE;
                    break; 
                case 1007:
                    val = INVALID_FRAME_PAYLOAD_DATA;
                    break;
                case 1008:
                    val = POLICY_VIOLATION;
                    break;
                case 1009:
                    val = MESSAGE_TOO_BIG;
                    break;
                case 1010:
                    val = MANDATORY_EXTENSION;
                    break;
                case 1011:
                    val = INTERNAL_SERVER_ERROR;
                    break;
                case 1015:
                    val = TLS_HANDSHAKE_FAILURE;
                    break; 
            }
            return val;
	    }
    }
    
    public ClosingReason getClosingReason()
    {
        if ( ! isCloseFrame())
        {
            throw new IllegalStateException("Cannot retrieve closing reason for frames other than Close frame");
        }
        
        if (payloadData.length < 2)
        {
            return ClosingReason.NO_STATUS_RECEIVED;
        }
        else
        {
            return ClosingReason.toClosingReason((payloadData[0] << 8) + payloadData[1]);
        }
    }
    
    public String toString(boolean showData, boolean showHex)
    {
        StringBuilder sb = new StringBuilder();
        
        sb.append("Frame type: ").append(opcode);
        sb.append("\nIs fin? ").append(fin);
        
        sb.append("\nIs masked? ").append(mask);
        if (mask)
        {
            sb.append("\nMasking key: ");
            for (byte b : maskingKey)
            {
                sb.append(String.format("%02X ", b));
            }
        }
        
        sb.append("\nData length: ").append(payloadLength).append(" bytes");

        if (showData)
        {
            sb.append("\nContent:\n");
            sb.append(payloadData);
        }
        
        if (showHex)
        {
            sb.append("\nHex representation:\n").append(payloadLength).append(" bytes");
        
            for (byte b : getBytes())
            {
                sb.append(String.format("%02X ", b));
            }
        }
        sb.append("\n");
        
        return sb.toString();
    }
    
    @Override
    public String toString()
    {
        return toString(false, false);
    }
    
    /*
    public Framedata(byte[] data)
    {
        if (data.length < 2)
        {
            throw new IllegalArgumentException("Incomplete or wrong data passed");
        }
        
        fin = (data[0] & 0x80) > 0 ? true : false;
        rsv1 = (data[0] & 0x40) > 0 ? true : false;
        rsv2 = (data[0] & 0x20) > 0 ? true : false;
        rsv3 = (data[0] & 0x10) > 0 ? true : false;
        mask = (data[1] & 0x80) > 0 ? true : false;
        
        opcode = Opcode.toOpcode((byte) (data[0] & 0x0F));
        
        int byteIterator = 2; // Must to introduce it, because from now on registers don't have clearly specified address
        
        payloadLength = data[1] & 0x7F;
        if (payloadLength == 126)
        {
            // Extended payload to 16 bits
            if (data.length < 4)
            {
                throw new IllegalArgumentException("Incomplete or wrong data passed");
            }
            
            payloadLength = (data[2] << 8) + data[3];
            byteIterator = 4;
        }
        else if (payloadLength == 127)
        {
            if (data.length < 10)
            {
                throw new IllegalArgumentException("Incomplete or wrong data passed");
            }
            
            // Extended payload to 64 bits
            payloadLength = (data[2] << 56) + (data[3] << 48) + (data[4] << 40)
                    + (data[5] << 32) + (data[6] << 24) + (data[7] << 16)
                    + (data[8] << 8) + data[9];
            byteIterator = 10;
            
            if (payloadLength > FRAME_CONTENT_MAX_LENGTH)
            {
                // Data of this length is not supported by our server
                throw new UnsupportedOperationException("Message of length " + payloadLength
                    + " is not supported. Maximum length supported: "
                    + (FRAME_CONTENT_MAX_LENGTH));
            }
        }
        
        if (mask)
        {
            if (data.length < byteIterator + 4)
            {
                throw new IllegalArgumentException("Incomplete or wrong data passed");
            }
            
            maskingKey = new byte[4];
            maskingKey[0] = data[byteIterator];
            maskingKey[1] = data[byteIterator + 1];
            maskingKey[2] = data[byteIterator + 2];
            maskingKey[3] = data[byteIterator + 3];
            byteIterator +=4;
        }
        
        // No extension supported. Extension block is 0 bytes long
        // Read data
        if (data.length != byteIterator + payloadLength)
        {
            throw new IllegalArgumentException("Incomplete or wrong data passed. Data length is not the same as specified in header");
        }
        
        if (payloadLength > 0)
        {
            payloadData = new byte[(int) payloadLength];
            if (mask)
            {
                // Reading data with unmasking
                for (int i = 0; i < payloadLength; i++)
                {
                    // Unmasking is done by doing XOR with masking key
                    payloadData[i] = (byte) (data[byteIterator + i] ^ maskingKey[i % 4]);
                }
            }
            else
            {
                // Reading without unmasking (according to RFC 6455 it should never happen)
                for (int i = 0; i < payloadLength; i++)
                {
                    payloadData[i] = data[byteIterator + i];
                }
            }
        }
        
    }
    //*/
    
    /*
    public static byte[] generateResponseFrame(boolean fin, Opcode opcode, byte[] data)
    {
        int length = 2; // Length of frame in bytes
        if (data.length > 125)
        {
            length += 2;
        }
        if (data.length > FRAME_CONTENT_MAX_LENGTH)
        {
            // Actually it is not supported and this condition should never be true
            throw new UnsupportedOperationException("Message of length " + data.length
                    + " is not supported. Maximum length supported: "
                    + (FRAME_CONTENT_MAX_LENGTH));
        }
        
        length += data.length;
        
        byte[] frame = new byte[length];
        
        if (fin)
        {
            frame[0] |= 0x80;
        }
        
        frame[0] |= opcode.code;
        
        int byteIterator = 2;
        if (data.length < 126)
        {
            frame[1] = (byte) data.length;
        }
        else if (data.length < 65536) // 2^16, value, which exceeds 16 bit register
        {
            frame[1] = (byte) 126;
            frame[2] = (byte) (data.length >> 8);
            frame[3] = (byte) (data.length);
            byteIterator = 4;
        }
        else
        {
            frame[1] = 127;
            frame[2] = 0;
            frame[3] = 0;
            frame[4] = 0;
            frame[5] = 0;
            frame[6] = (byte) (data.length >> 24);
            frame[7] = (byte) (data.length >> 16);
            frame[8] = (byte) (data.length >> 8);
            frame[9] = (byte) (data.length);
            byteIterator = 10;
        }
        
        System.arraycopy(data, 0, frame, byteIterator, data.length);

        return frame;
    }
    //*/
    
    /*
    public byte[] generateFrame()
    {
        int length = 2; // Length of frame in bytes
        if (payloadLength > 125)
        {
            length += 2;
        }
        if (payloadLength > FRAME_CONTENT_MAX_LENGTH)
        {
            // Actually it is not supported and this condition should never be true
            throw new UnsupportedOperationException("Message of length " + payloadLength
                    + " is not supported. Maximum length supported: "
                    + (FRAME_CONTENT_MAX_LENGTH));
        }
        
        if (mask)
        {
            length += 4;
        }
        length += payloadData.length;
        
        byte[] frame = new byte[length];
        
        if (fin)
        {
            frame[0] |= 0x80;
        }
        if (rsv1)
        {
            frame[0] |= 0x40;
        }
        if (rsv2)
        {
            frame[0] |= 0x20;
        }
        if (rsv3)
        {
            frame[0] |= 0x10;
        }
        
        frame[0] |= opcode.code;
        
        int byteIterator = 2;
        if (payloadLength < 126)
        {
            frame[1] = (byte) payloadLength;
        }
        else if (payloadLength < 65536) // 2^16, value, which exceeds 16 bit register
        {
            frame[1] = (byte) 126;
            frame[2] = (byte) (payloadLength >> 8);
            frame[3] = (byte) (payloadLength);
            byteIterator = 4;
        }
        else
        {
            frame[1] = (byte) 127;
            frame[2] = (byte) (payloadLength >> 56);
            frame[3] = (byte) (payloadLength >> 48);
            frame[4] = (byte) (payloadLength >> 40);
            frame[5] = (byte) (payloadLength >> 32);
            frame[6] = (byte) (payloadLength >> 24);
            frame[7] = (byte) (payloadLength >> 16);
            frame[8] = (byte) (payloadLength >> 8);
            frame[9] = (byte) (payloadLength);
            byteIterator = 10;
        }
        
        if (mask)
        {
            frame[1] |= 0x80;
            frame[byteIterator] = maskingKey[0];
            frame[byteIterator + 1] = maskingKey[1];
            frame[byteIterator + 2] = maskingKey[2];
            frame[byteIterator + 3] = maskingKey[3];
            byteIterator += 4;
        }
        
        if (mask)
        {
            for (int i = 0; i < payloadLength; i++)
            {
                // Mask the message
                frame[byteIterator + i] = (byte) (payloadData[i] ^ maskingKey[i % 4]);
            }
        }
        else
        {
            System.arraycopy(payloadData, 0, frame, byteIterator, (int) payloadLength);
        }
        
        return frame;
    }//*/
}
